[Generic表]TDRBlob内嵌Protobuf部分字段读写

[TDR Generic表][Go SDK]TDR Blob内嵌Protobuf部分字段读写

字段路径的完整语法、读写语义和限制见《TDR Blob 内嵌 Protobuf 部分字段读写》。本文介绍 Go SDK 接口与接入示例。

1. 接口说明

TDR 表的 Blob 字段(char/Byte 数组 + refer 长度字段)里如果保存的是 Protobuf 序列化数据, 可以只读取或只更新 Blob 中指定的 PB 字段,而不必整段回读、改完再整段回写。

三个接口对应三个命令字:

接口命令字值说明
DoPBFieldGetTcaplusApiPBFieldGetReq0x0067读取 Blob 中指定的 PB 字段
DoPBFieldUpdateTcaplusApiPBFieldUpdateReq0x0069更新 Blob 中指定的 PB 字段,记录不存在会报错,不会自动插入
DoPBBatchFieldGetTcaplusApiPBBatchFieldGetReq0x0075多个主键共用一组字段路径批量读取

Blob 中 Protobuf 字段的自增(TcaplusApiPBFieldIncreaseReq)在 TDR 表上不支持。

2. 版本要求

  • Go SDK 的 v0.6.36 已包含本功能;使用其他版本时,请确认提供本文所述接口。
  • 服务端须为 Tcapsvr 3.73.0 2026-10-10 以后的版本。旧版后端在这三个命令上返回 SVR_ERR_FAIL_TO_FIND_META,需要升级后端。

3. 准备工作

参见准备工作文档,完成使用该接口前的准备工作,并创建TDR Generic表。 service_info表service_info.xml

本特性要求表里有一个 Blob 字段 + 它的 refer 长度字段,service_info 表中对应的是:

<entry name="routeinfo_len"     type="uint"   defaultvalue="0" desc="路由规则信息长度" />
<entry name="routeinfo"         type="char"   count="1024" refer="routeinfo_len" desc="路由规则信息" />

routeinfo 是保存 PB 的 Blob 字段,routeinfo_len 是它的 refer 长度字段, 这两个字段在下面的示例里分别对应 Go 结构体的 Routeinfo 和 Routeinfo_Len。

Blob 里的 PB 定义示例如下(RouteInfo后面都是使用该proto进行示例演示):

syntax = "proto3";
package routeinfo;

message RouteInfo {
  uint32 version = 1;
  string strategy = 2;
  Weight weight = 3;
  repeated string tags = 4;
  repeated Instance instances = 5;
  map<int64, Instance> instance_map = 6;
  map<string, string> settings = 7;
}
message Weight { uint32 cpu = 1; uint32 memory = 2; }
message Instance { string addr = 1; uint32 port = 2; uint32 weight = 3; }

该proto表定义不用上传到Tcaplus服务端,只存在于客户端

该 PB 定义与生成代码见示例目录。

4. PB部分字段操作方式

4.1. 方案1

部分字段读写的核心是字段路径,服务端没有业务的 .proto,只认数字 tag 路径,直接写PB数字tagid:

// 操作 PB RouteInfo 的 version 字段
// 操作 PB RouteInfo 的 strategy 字段
// 操作 PB RouteInfo 的 weight.cpu 字段(嵌套 message 子字段)
// 操作 PB RouteInfo 的 instance_map 字段的 map 元素 1001
// 操作 PB RouteInfo 的 settings 字段的 map 元素 region
// 操作 tdr 原生字段 filterdata
opt := &option.TDROpt{FieldNames: []string{"routeinfo.1", "routeinfo.2", "routeinfo.3.1",
    "routeinfo.6[1001]", "routeinfo.7['region']", "filterdata"}}

4.2. 方案2

直接使用tagid操作字段,不够直观,维护不方便,因此我们增加了PB字段名到 tagid 映射的适配层用法,方便维护。

字段名到 tag 路径的转换用 tdrpb 包:

paths, err := tdrpb.BuildPaths(
    // 操作 routeinfo 这个 Blob 里的 RouteInfo PB 字段
    tdrpb.Blob("routeinfo", (*routeinfo.RouteInfo)(nil),
        "version",            // routeinfo.1
        "strategy",           // routeinfo.2
        "weight.cpu",         // routeinfo.3.1,嵌套 message 子字段
        "instance_map[1001]", // routeinfo.6[1001],map 指定 key
        "settings['region']", // routeinfo.7['region'],字符串 key 的 map value
    ),

    // 如果 TDR 里还有别的 Blob 存放 PB Message,比如 extra.bin,可以再加一个分组
    // tdrpb.Blob("extra.bin", (*xxx.Other)(nil), "version"),

    // 操作原生 TDR 一级字段,原样透传
    tdrpb.Raw("filterdata"),
)
if err != nil {
    return
}

// 最终调用会转换成方案1的调用
opt := &option.TDROpt{FieldNames: paths}

Blob 中的 PB 参数只用于获取类型,不提供更新值。更新数据由增量 PB 提供,见下一节。删除 map 元素时使用 tdrpb.Pop("routeinfo", (*routeinfo.RouteInfo)(nil), "instance_map[1001]"),生成 POP routeinfo.6[1001];不通过 opt.Operation 传递。

FieldNames 中无需手动加入 routeinfo、routeinfo_len:SDK 自动补齐一级字段及所需 refer。一次请求也可以通过多个 Blob 分组选择不同 Blob 中的字段。

5. 完整示例代码

示例代码见示例目录, 本特性相关的示例文件为 pbfieldprepare.go、pbfieldget.go、pbfieldupdate.go、pbbatchfieldget.go, 入口在 main.go 中。下面的准备数据、5.1、5.2、5.3 分别对应示例文件里的 pbFieldPrepareExample、pbFieldGetExample、pbFieldUpdateExample、pbBatchFieldGetExample, 响应处理对应 5.4。

Blob 里必须是合法的 PB 编码,普通写入的裸字符串服务端解析不了。先整段写一份完整 PB(对应示例文件的 pbFieldPrepareExample):

func pbFieldPrepareExample() {
    // 1. 构造完整 PB
    full := &routeinfo.RouteInfo{Version: 1, Strategy: "round_robin", /* ... */}

    // 增量/整段 PB 都用 partial 语义序列化,proto2 缺 required 字段时也能正常处理
    buf, err := tdrpb.MarshalPartial(full)
    if err != nil {
        fmt.Println(err.Error())
        return
    }

    // 2. 填主键,Blob 和它的 refer 长度字段一起整段写入
    data := service_info.NewService_Info()
    data.Gameid = "dev"
    data.Envdata = "oa"
    data.Name = "com"
    data.Routeinfo = buf
    data.Routeinfo_Len = uint32(len(buf))

    // 3. 用 Replace 而不是 Insert,示例可以反复跑
    if err = client.DoReplace(TableName, data, nil); err != nil {
        fmt.Println(err.Error())
        return
    }
}

5.1 读取部分字段

对应示例文件的 pbFieldGetExample、pbFieldGetContainerExample。

func pbFieldGetExample() {
    // 1. 只填主键,value 字段由服务端按字段路径回填,业务不需要预先序列化 Blob
    data := service_info.NewService_Info()
    data.Gameid = "dev"
    data.Envdata = "oa"
    data.Name = "com"

    /////////////////////////////////////////////////////////////////////
    // 2. 唯一与适配层相关的步骤:按 PB 字段名构造字段路径
    opt, err := &option.TDROpt{}, error(nil)
    if opt.FieldNames, err = tdrpb.BuildPaths(
        // routeinfo 这个 Blob 里的 RouteInfo PB 字段
        tdrpb.Blob("routeinfo", (*routeinfo.RouteInfo)(nil),
            "version",    // routeinfo.1
            "strategy",   // routeinfo.2
            "weight.cpu", // routeinfo.3.1
        ),

        // 原生 TDR 一级字段,原样透传
        tdrpb.Raw("filterdata"),
    ); err != nil {
        fmt.Println(err.Error())
        return
    }

    // 或者使用方案1的tagid方式调用
    // opt.FieldNames = []string{"routeinfo.1", "routeinfo.2", "routeinfo.3.1", "filterdata"}
    ///////////////////////////////////////////////////////////////////

    // 3. 发起请求,结果直接回填到 data
    if err = client.DoPBFieldGet(TableName, data, opt); err != nil {
        fmt.Println(err.Error())
        return
    }

    // 4. Routeinfo_Len 是本次部分 PB 的长度,不是记录里完整 Blob 的长度
    partial := &routeinfo.RouteInfo{}
    if err = tdrpb.UnmarshalPartial(data.Routeinfo, data.Routeinfo_Len, partial); err != nil {
        fmt.Println(err.Error())
        return
    }

    // 5. 只有请求过的字段有值,未请求的字段是未设置状态,返回的部分记录不能当完整记录用
    fmt.Println(partial.GetVersion(), partial.GetStrategy(), partial.GetWeight().GetCpu(), data.Filterdata)
}

BuildPaths 最终生成的是 routeinfo.1、routeinfo.2、routeinfo.3.1 和 filterdata,服务端只回填这四个路径对应的值。

与 5.2 的更新不同,读取不需要业务构造增量 PB:只填主键,Blob 内容由服务端按字段路径写回,响应里直接解析即可。filterdata 是原生 TDR 字段,不经过 PB 解析,和三个 PB 路径一起传入同一次 SetFieldNames。

map 指定 key 和 repeated 指定下标(pbFieldGetContainerExample)写法相同,只是路径不同:

opt, err := &option.TDROpt{}, error(nil)
if opt.FieldNames, err = tdrpb.BuildPaths(
    tdrpb.Blob("routeinfo", (*routeinfo.RouteInfo)(nil),
        "instance_map[1001].addr", // routeinfo.6[1001].1,map 元素的子字段
        "instances[0]",            // routeinfo.5#[0],repeated 下标用 #[]
        "settings",                // routeinfo.7,整组读取
    ),
); err != nil {
    fmt.Println(err.Error())
    return
}

// 读不存在的 map key 返回 COMMON_ERR_ELEMENT_NOT_EXIST,不是一次成功的默认值读取
if err = client.DoPBFieldGet(TableName, data, opt); err != nil {
    fmt.Println(err.Error())
    return
}

5.2 更新部分字段

对应示例文件的 pbFieldUpdateExample(部分更新)、pbFieldUpdateMapExample(map 元素覆盖与删除)。

func pbFieldUpdateExample() {
    // 1. 只填主键
    data := service_info.NewService_Info()
    data.Gameid = "dev"
    data.Envdata = "oa"
    data.Name = "com"

    // 2. 业务构造增量 PB,只带本次要写的字段
    delta := &routeinfo.RouteInfo{Version: 3, Strategy: "weighted_round_robin"}

    // 3. 增量 PB 序列化到 TDR Blob
    data.Routeinfo = make([]byte, 1024) // Init 不会给 Blob 分配空间,tdr_count 是 1024
    n, err := tdrpb.MarshalPartialTo(data.Routeinfo, delta)
    if err != nil {
        fmt.Println(err.Error())
        return
    }
    data.Routeinfo_Len = uint32(n)

    /////////////////////////////////////////////////////////////////////
    // 4. 唯一与适配层相关的步骤:按 PB 字段名构造字段路径
    opt := &option.TDROpt{}
    if opt.FieldNames, err = tdrpb.BuildPaths(
        tdrpb.Blob("routeinfo", (*routeinfo.RouteInfo)(nil),
            "version",  // routeinfo.1
            "strategy", // routeinfo.2
        ),
    ); err != nil {
        fmt.Println(err.Error())
        return
    }

    // 或者使用方案1的tagid方式调用
    // opt.FieldNames = []string{"routeinfo.1", "routeinfo.2"}
    ///////////////////////////////////////////////////////////////////

    // 5. 增量只带本次要写的字段,Blob 里其余字段服务端原样保留
    if err = client.DoPBFieldUpdate(TableName, data, opt); err != nil {
        fmt.Println(err.Error())
        return
    }
}

BuildPaths 最终生成的是 routeinfo.1 和 routeinfo.2,只有这两个路径允许被修改, Blob 中其余字段服务端原样保留。路径决定修改范围,增量提供新值。

删除 map 元素改用 Pop 分组,此时不需要增量,Blob 留空、长度填 0:

opt, err := &option.TDROpt{}, error(nil)
if opt.FieldNames, err = tdrpb.BuildPaths(
    // POP routeinfo.6[1001]
    tdrpb.Pop("routeinfo", (*routeinfo.RouteInfo)(nil), "instance_map[1001]"),
); err != nil {
    fmt.Println(err.Error())
    return
}

// 删除不提供新值,Routeinfo 留空、Routeinfo_Len 为 0
data := service_info.NewService_Info()
data.Gameid = "dev"
data.Envdata = "oa"
data.Name = "com"

if err = client.DoPBFieldUpdate(TableName, data, opt); err != nil {
    fmt.Println(err.Error())
    return
}

TDR 的 tinyint 数组会被生成为 []int8,这种表换用 tdrpb.MarshalPartialToInt8 / tdrpb.UnmarshalPartialInt8,参数含义一致。

5.3 批量读取

对应示例文件的 pbBatchFieldGetExample。

func pbBatchFieldGetExample() {
    // 1. 每条记录只填主键,单次最多 1024 条
    var dataSlice []record.TdrTableSt
    for i := 0; i < 10; i++ {
        data := service_info.NewService_Info()
        data.Gameid = "dev"
        data.Envdata = "oa"
        data.Name = fmt.Sprintf("%d", i)
        dataSlice = append(dataSlice, data)
    }

    /////////////////////////////////////////////////////////////////////
    // 2. 唯一与适配层相关的步骤:所有主键共用同一组字段路径,只构造一次
    opt, err := &option.TDROpt{}, error(nil)
    if opt.FieldNames, err = tdrpb.BuildPaths(
        tdrpb.Blob("routeinfo", (*routeinfo.RouteInfo)(nil), "version", "strategy"),
        tdrpb.Raw("filterdata"),
    ); err != nil {
        fmt.Println(err.Error())
        return
    }

    // 或者使用方案1的tagid方式调用
    // opt.FieldNames = []string{"routeinfo.1", "routeinfo.2", "filterdata"}
    ///////////////////////////////////////////////////////////////////

    // 3. 请求级成功不等于每条记录都成功,需要逐条检查 opt.BatchResult
    if err = client.DoPBBatchFieldGet(TableName, dataSlice, opt); err != nil {
        fmt.Printf("batch field get: %s\n", err)
        // 可能只有部分记录失败,继续检查已经返回的逐条结果。
    }
    if len(opt.BatchResult) != len(dataSlice) {
        return // 请求构造失败等情况下,逐条结果尚未建立。
    }

    // 4. 按主键回填到 dataSlice 的对应下标,逐条解析部分 PB
    for i, data := range dataSlice {
        if opt.BatchResult[i] != nil {
            fmt.Printf("record %d failed: %s\n", i, opt.BatchResult[i].Error())
            continue
        }
        row := data.(*service_info.Service_Info)
        partial := &routeinfo.RouteInfo{}
        if err = tdrpb.UnmarshalPartial(row.Routeinfo, row.Routeinfo_Len, partial); err != nil {
            fmt.Println(err.Error())
            continue
        }
        fmt.Printf("record %d version %d, filterdata %s\n", i, opt.BatchVersion[i], row.Filterdata)
    }
}

BuildPaths 最终生成的是 routeinfo.1、routeinfo.2 和 filterdata。

与 5.1 的读取一样只填主键,区别是一次请求带多条;所有主键共用同一组字段路径, BuildPaths 只调用一次,单次最多 1024 条主键。 响应不保证顺序,按主键回填到 dataSlice 的对应下标;每条记录各自返回结果, 用 opt.BatchResult[i] 判断这一条是否成功,版本看 opt.BatchVersion[i]。 分包由服务端通过响应里的 LeftNum 驱动,SDK 已经收完所有分包,不需要设置 MultiFlag。

5.4 响应与逐条结果

Go 是同步接口,没有独立的响应对象:成功时结果直接回填到传入的 data / dataSlice, 失败时通过接口返回值和 opt.BatchResult 给出错误。三个接口共用同一套处理逻辑,有三点需要注意:

  • data.Routeinfo_Len 是本次部分 PB 的长度,不是记录里完整 Blob 的长度;
  • 解析出的 partial 只包含本次请求的字段,不能再拿去整段覆盖 Blob;
  • 批量请求里每条记录各自返回结果,opt.BatchResult[i] 才是这一条的结果, 接口返回值是请求级结果,成功也不代表每条记录都成功。

6. option 支持情况

opt 必填且 opt.FieldNames 不能为空。不支持的选项会返回错误;批量 Condition 返回 GEN_ERR_INVALID_ARGUMENTS。

optionFieldGetFieldUpdateBatchFieldGet
FieldNames必填必填必填
Condition支持支持不支持(请求体没有条件字段)
Timeout / Ctx / Flags / UserBuff支持支持支持
Version支持,入参兼出参支持拒绝,版本只从 BatchVersion 出参
VersionPolicy拒绝支持拒绝
ResultFlagForSuccess / ResultFlagForFail拒绝支持拒绝
BatchResult / BatchVersion不适用不适用支持
MultiFlag / Operation / ResultFlag / IncField / TTL / Limit / Offset / ExpireTime 等拒绝拒绝拒绝

Version 在 DoPBFieldGet 上既是入参也是出参,响应回来时被改写为记录的当前版本。

7. 错误码

本地检查(接口返回值):

场景错误码
opt 为 nil、FieldNames 为空、使用了不支持的 optionParameterInvalid
路径为空或超过 1023 字节API_ERR_OVER_MAX_FIELD_NAME_LEN
补齐后字段数超过 256API_ERR_OVER_MAX_VALUE_FIELD_NUM
路径重复或父子路径冲突、未先 SetDataAPI_ERR_PARAMETER_INVALID
路径的一级字段不是本表的 value 字段API_ERR_FIELD_NOT_EXSIST
PB 字段不存在API_ERR_FIELD_NOT_EXSIST
map / repeated / 嵌套 message 类型不匹配API_ERR_FIELD_TYPE_NOT_MATCH
Blob 容量不足API_ERR_OVER_MAX_FIELD_VALUE_LEN

发送后的结果看接口返回值;批量还要看每条 opt.BatchResult。常见服务端错误码:

错误码含义
TXHDB_ERR_RECORD_NOT_EXIST记录不存在
COMMON_ERR_ELEMENT_NOT_EXIST指定的 map key 或 repeated 下标不存在
COMMON_ERR_CONDITION_NOT_MATCHED条件不成立
SVR_ERR_FAIL_INVALID_VERSION版本校验失败

详见错误码含义和处理方法。

8. 注意事项

  1. 只支持 Generic 表,List 表只能由服务端拒绝,SDK 本地拿不到表类型。
  2. Blob 里必须是当前业务 descriptor 对应的 PB 编码,历史数据的 schema 兼容性由业务保证。
  3. 返回的 routeinfo_len 是本次部分 PB 的长度,不是记录中完整 Blob 的长度; 解析出的 PB 对象只包含本次请求的字段,不能直接用于整段 Blob 覆盖。
  4. SDK 不做 PB 编解码,增量 PB 的构造、Blob 的写入、响应 Blob 的解析都由业务负责, tdrpb 只提供 MarshalPartial / MarshalPartialTo / UnmarshalPartial 等便利函数。
  5. 路径里的一级字段名取 TDR 表定义中的字段名(tdr_field),不是 Go 结构体字段名。
  6. 字段名使用 .proto 中的原始 field name,不使用 JSON name。
  7. 嵌套结构体里的 Blob(如 extra.bin)也支持,SDK 自动补齐其所属的一级结构体字段。

9. 其它参考文档

TDR Blob 内嵌 Protobuf 部分字段读写

[TDR Generic表][C++ SDK]TDR Blob内嵌Protobuf部分字段读写

[TDR Generic表][Go SDK]查询单条数据

[TDR Generic表][Go SDK]条件过滤和更新说明